package com.mt.roll.opcua.s7.my.s7connector.enmuc;

import com.mt.roll.opcua.s7.my.s7connector.api.DaveArea;
import com.mt.roll.opcua.s7.my.s7connector.api.S7Connector;
import com.mt.roll.opcua.s7.my.s7connector.api.factory.S7ConnectorFactory;
import com.mt.roll.opcua.s7.my.s7connector.api.utils.ByteUtils;
import com.mt.roll.opcua.s7.my.s7connector.type.PlcVar;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

/**
 * @Desc: ""
 * @Author: caixiang
 * @DATE: 2022/1/15 13:01
 */
public enum S7Client {
    //TODO 步骤1  这里是配置多PLC 的，，，有多个plc 就在这里配置一个枚举类
    //1500    虽然设备上显示机架-0  插槽-1（这个是默认的）     但是我们这里 rack、slot 都是0，反正我们这里就让电控rack和solt都是默认的 然后我们这边都配置0就行了。
    S7_1500("192.168.0.51",0,1,3,PlcVarActual.HeartBeat),
    //1500  机架-0  插槽-1
    S7_1200("192.168.0.52",0,1,3,PlcVarActual.HeartBeatFor1200)
    //后续 在这里扩展 多PLC应用。



    ;
    private String host;
    //默认 0   机架号
    private Integer rack;
    //默认 0
    private Integer slot;

    //心跳变量，如果plc没有让电控的人加一个，这个是必填的
    private PlcVarActual heartBeat;

    private List<S7Connector> connections;
    //coreSize 是线程池的大小
    private Integer coreSize;
    //pickOne 就是一个初始化 的轮询取余值
    private int pickOne;
    private static final Logger logger = LoggerFactory.getLogger(S7Client.class);

    private ScheduledExecutorService executor;
    private ScheduledExecutorService ping_fail_check;

    //coreSize 是线程池的数量
    S7Client(String host, Integer rack, Integer slot,Integer coreSize,PlcVarActual heartBeat){
        this.host = host;
        this.rack = rack;
        this.slot = slot;
        this.pickOne = 0;

        this.executor = Executors.newScheduledThreadPool(1);
        this.ping_fail_check = Executors.newScheduledThreadPool(1);

        this.coreSize = coreSize;
        this.heartBeat = heartBeat;
        connections = new ArrayList<>();
        connectionPool();

        ping();

        check_ping();
    }



    /**
     * PlcVar(byte[])  转  java对象    对照表
     * 单体变量
     * Bool       ===>      Boolean
     * LREAL      ===>      Double
     * REAL       ===>      Float
     * DATE       ===>      String(yyyy-MM-dd 这种形式的类型)
     * DTL        ===>      String("年-月-日-工作日-时-分-秒" 这种格式)
     * TIME       ===>      Integer(单位 ms)
     * USINT      ===>      Integer
     * SINT       ===>      Integer
     * UINT       ===>      Integer
     * INT        ===>      Integer
     * DINT       ===>      Integer
     * UINT       ===>      Long
     * Byte       ===>      Integer(有符号)(默认)
     *                     Integer(无符号)(后续扩展)
     * Char       ===>      Character
     * WChar      ===>      Character
     * String     ===>      String                      （特殊）


     * 数组变量
     * BoolArray      ===>      List<Boolean>
     * ByteArray      ===>      List<Byte>
     * WordArray      ===>      List<Integer>
     * DWordArray     ===>      List<Integer>
     * CharArray      ===>      List<Character>
     * SIntArray      ===>      List<Integer>
     * IntArray       ===>      List<Integer>
     * DIntArray      ===>      List<Integer>
     * UIntArray      ===>      List<Integer>
     * USIntArray     ===>      List<Integer>
     * UDIntArray     ===>      List<Long>
     * StringArray    ===>      String[]        （特殊）
     *
     *
     * */
    public Object read(DaveArea area, Integer areaNumber, Integer byteOffset, Integer bitOffset, Integer length, Integer strSizes, PlcVar type) throws UnsupportedEncodingException, ParseException {
        S7Connector connector = getConnector();
        //String 类型比较特殊。 String[] 也是同理，Sring数组里面的子项 也是有两个字节的 readBytes。
        if(type.equals(PlcVar.STRING)){
            Integer readBytes = 2;
            byte[] read = connector.read(
                    area,
                    areaNumber,
                    readBytes,
                    byteOffset,
                    bitOffset,
                    type.getTransportSize()
            );

            Integer allLength = Integer.valueOf(read[1])+2;
            byte[] readF = connector.read(
                    area,
                    areaNumber,
                    allLength,
                    byteOffset,
                    bitOffset,
                    type.getTransportSize()
            );
            return type.toObject(readF);
        }else if(type.equals(PlcVar.STRING_Array)){
            Integer arrayLength =  length;
            Integer strSize =  strSizes;

            byte[] read = connector.read(
                    area,
                    areaNumber,
                    arrayLength*(strSize+2),
                    byteOffset,
                    bitOffset,
                    type.getTransportSize()
            );
            return ByteUtils.toStrArray(read, arrayLength, strSize);
        }else {
            Integer readBytes = type.getTransportSize().getSizeInBytes() * length;
            byte[] read = connector.read(
                    area,
                    areaNumber,
                    readBytes,
                    byteOffset,
                    bitOffset,
                    type.getTransportSize()
            );
            return type.toObject(read);
        }
    }

    /**
     *
     * eg :
     *      Object newValue = Boolean.FALSE
     *      s7Service.write(PlcVarActual.HeartBeat, newValue, S7Client.S7_1200);
     *
     * PlcVar(byte[])  转  java对象    对照表
     * 单体变量
     * Bool       ===>      Object newValue = Boolean.FALSE
     * LREAL      ===>      Object newValue = Boolean.FALSE
     * REAL       ===>      Object newValue = Boolean.FALSE
     * DATE       ===>      暂时没需求（有问题找我）
     * DTL        ===>      暂时没需求（有问题找我）
     * TIME       ===>      暂时没需求（有问题找我）
     * USINT      ===>      Object newValue = new Integer(1)
     * SINT       ===>      Object newValue = new Integer(1)
     * UINT       ===>      Object newValue = new Integer(1)
     * INT        ===>      Object newValue = new Integer(1)
     * DINT       ===>      Object newValue = new Integer(1)
     * UINT       ===>      Object newValue = new Integer(1)
     * Byte       ===>      Object newValue = 0x11
     *
     * Char       ===>      Object newValue = 'a'
     * WChar      ===>      Object newValue = '菜'
     * String     ===>      Object newValue = '你好啊'      （特殊）


     * 数组变量
     *      注意：在write的时候，你write的数量  一定要和  plc中存在的数量一一对应
     * BoolArray      ===>      boolean[] booleanArray = new boolean[2];  .... 赋予值
     * ByteArray      ===>      byte[] write_byteArrays = new byte[2];
     * WordArray      ===>      short[] shortArrays_content = new short[2];
     * DWordArray     ===>      int[] intArrays_content = new int[2];
     * CharArray      ===>      char[] charArrays_content = new char[2];
     * SIntArray      ===>      int[] sintArrays_content = new int[2];
     * IntArray       ===>      int[] iintArrays_content = new int[2];
     * DIntArray      ===>      int[] dintArrays_content = new int[2];
     * UIntArray      ===>      int[] uintArrays_content = new int[3];
     * USIntArray     ===>      int[] usintArrays_content = new int[3];
     * UDIntArray     ===>      int[] udintArrays_content = new int[3];
     * StringArray    ===>      String[] stringArrays_content = new String[3];
     * //如果有其他数据类型  这里没有找我扩展
     *
     *
     * */
    public void write(DaveArea area, Integer areaNumber, Integer byteOffset, Integer bitOffset,Integer strSize, PlcVar type, Object newValue){
        S7Connector connector = getConnector();

        //String 类型比较特殊。 String[] 也是同理，Sring数组里面的子项 也是有两个字节的 readBytes。
        if(type.equals(PlcVar.STRING)){
            connector.write(
                    area,
                    areaNumber,
                    byteOffset,
                    bitOffset,
                    ByteUtils.strToBytes(newValue.toString(), strSize),
                    type
            );
        }else if(type.equals(PlcVar.STRING_Array)){
            //todo here 检查 read write service
            connector.write(
                    area,
                    areaNumber,
                    byteOffset,
                    bitOffset,
                    ByteUtils.strArrayToBytes((String[])newValue, strSize),
                    type
            );
        }else {
            byte[] bytes = type.toBytes(newValue);
            connector.write(
                    area,
                    areaNumber,
                    byteOffset,
                    bitOffset,
                    bytes,
                    type
            );
        }
    }




    public S7Connector getConnector() {
        int size = connections.size();
        S7Connector s7Connector = connections.get((pickOne + size) % size);
        pickOne+=1;
        pickOne = (pickOne)%size;
        return s7Connector;
    }
    private S7Connector connect(String host,Integer rack,Integer slot ){
        try {
            S7Connector connector = S7ConnectorFactory
                    .buildTCPConnector()
                    .withHost(host)
                    .withRack(rack) //optional   rack 是机架号
                    .withSlot(slot) //optional   slot 是插槽号
                    .build();
            return connector;
        }catch (Exception e){
            logger.info("创建S7Connector  连接失败");
            return null;
        }

    }

    private void connectionPool(){
        for(int i=0;i<coreSize;i++){
            connections.add(connect(host,rack,slot));
        }
        //todo 在plc上新增一个 变量来解决 心跳问题 okok
    }

    private void check_ping(){
        ping_fail_check.execute(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if(connections.size()!=coreSize){
                        int c = coreSize-connections.size();
                        for(int z=0;z<c;z++){
                            S7Connector connect = connect(host, rack, slot);
                            if(connect!=null){
                                logger.info("host:"+host +" ;;(check_ping())    检测到有断线 ==》  现在断线恢复成功");
                                connections.add(connect);
                            }else {
                                logger.info("host:"+host +" ;;(check_ping())    检测到有断线 ==》  现在断线恢复失败");
                            }

                        }
                    }
                    try {
                        Thread.sleep(10000);
                    }catch (Exception e){
                        logger.info("host:"+host +" ;;(check_ping())   Thread.sleep 异常，异常原因："+e.getMessage());
                    }
                }
            }
        });
    }

    //todo 当网络断开之后，下面这个ping() 循环不生效。。  可能是read = connector.read( 出来是null，但还是继续通过了，也可能是 ，，当连接全部断开后 getConnector(); 应该是取不到数据的。。也就会报null。这个明天再看看
    private void ping(){
       executor.execute(new Runnable() {
            @Override
            public void run() {
                while (true){
                    for(int i=0;i<coreSize;i++){
                        S7Connector connector = getConnector();
                        try {
                            //只要没有报异常  都是通讯正常的。
                            byte[] read = connector.read(
                                    heartBeat.getArea(),
                                    heartBeat.getAreaNumber(),
                                    heartBeat.getType().getTransportSize().getSizeInBytes(),
                                    heartBeat.getByteOffset(),
                                    heartBeat.getBitOffset(),
                                    heartBeat.getType().getTransportSize());
                            System.out.println("host:"+host +" ;; "+connector.hashCode()+" :  ping");
                            Thread.sleep(100);
                        }catch (Exception e){
                            logger.info("host:"+host +" ;;(ping)    "+connector.hashCode()+" :  connection error"+"errMessage is : "+e.getMessage());
                            //先把 socket close掉
                            try {
                                connector.close();
                                connections.remove(connector);
                                //如果是网络波动照成的socket断开。 等个1S 再重连试试
                                Thread.sleep(100);
                            }catch (Exception ee){
                                logger.info("host:"+host +" ;;(ping)    "+"connector.close() 出现异常，errMessage is : "+ee.getMessage());
                            }
                        }

                        //todo 把之前的连接close 掉，然后新增一个连接到connections
                        if(connections.size()!=coreSize){
                            int c = coreSize-connections.size();
                            for(int z=0;z<c;z++){
                                S7Connector connect = connect(host, rack, slot);
                                if(connect!=null){
                                    logger.info("host:"+host +" ;;(ping)    "+"ping时候出现异常，尝试重连， 重连成功!!");
                                    connections.add(connect);
                                }else {
                                    logger.info("host:"+host +" ;;(ping)    "+"ping时候出现异常，尝试重连， 重连时候还是 出现异常。。");
                                }
                            }
                        }
                    }

                    try {
                        Thread.sleep(30000);
                    }catch (Exception e){
                        e.printStackTrace();
                        logger.info("host:"+host +" ;;(ping)    "+e.getMessage());
                    }
                }
            }
        });
    }
}